package org.xx.armory.commons;

import org.apache.commons.beanutils.PropertyUtils;
import org.xx.armory.config.ConfigurationException;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;
import java.util.WeakHashMap;

import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.xx.armory.commons.Validators.notBlank;

/**
 * 创建对象相关的工具类
 */
public final class BuilderUtils {
    private static final WeakHashMap<String, Constructor<?>> CONSTRUCTORS_CACHE = createConstructorsCache();

    private BuilderUtils() {
        throw new UnsupportedOperationException();
    }

    private static WeakHashMap<String, Constructor<?>> createConstructorsCache() {
        return new WeakHashMap<>();
    }

    public static Object build(
            String className,
            String factoryName,
            Properties properties
    ) {
        className = trimToEmpty(className);
        factoryName = trimToEmpty(factoryName);

        try {
            if (factoryName.isEmpty()) {
                return buildDirectly(className, properties);
            } else {
                return buildFromFactory(factoryName, properties);
            }
        } catch (ReflectiveOperationException ex) {
            throw new IllegalStateException("cannot build object", ex);
        }
    }

    /**
     * 使用指定类型的默认构造函数创建对象。
     *
     * @param className
     *         类型全名，自动去掉首尾空格。
     * @param properties
     *         需要为生成的对象设置属性的集合。
     * @return 已构造的对象。
     * @throws IllegalArgumentException
     *         如果参数{@code className}是{@code null}或者只包含空白字符。
     * @throws ClassNotFoundException
     *         如果找不到指定的类型。
     * @throws IllegalAccessException
     *         如果无法访问指定类型的构造器或者属性访问器。
     * @throws InvocationTargetException
     *         如果调用构造器或者设置属性值出错。
     * @throws InstantiationException
     *         如果调用构造器出错。
     * @throws NoSuchMethodException
     *         如果找不到属性访问器。
     */
    public static Object buildDirectly(
            String className,
            Properties properties
    )
            throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        className = notBlank(className, "className").trim();

        Constructor<?> constructor = CONSTRUCTORS_CACHE.get(className);
        if (constructor == null) {
            constructor = Class.forName(className).getConstructor();
            if (constructor == null) {
                throw new ConfigurationException("cannot found no-arguments constructor: " + className + ".<init>()");
            }
            constructor.setAccessible(true);
            CONSTRUCTORS_CACHE.put(className, constructor);
        }

        final Object obj = constructor.newInstance();
        if (properties != null && !properties.isEmpty()) {
            for (final String key : properties.stringPropertyNames()) {
                final String value = properties.getProperty(key, "");
                PropertyUtils.setProperty(obj, key, value);
            }
        }
        return obj;
    }

    /**
     * 通过工厂构造目标对象。
     *
     * @param factoryName
     *         工厂类型全名，自动去掉首尾空格。
     * @param properties
     *         需要为生成的对象设置属性的集合。
     * @return 目标对象。
     * @throws IllegalArgumentException
     *         如果参数{@code factoryName}是{@code null}或者只包含空白字符。
     * @throws ClassNotFoundException
     *         如果找不到指定的工厂类型。
     * @throws IllegalAccessException
     *         如果无法访问指定工厂类型的工厂器。
     * @throws NoSuchMethodException
     *         如果找不到指定的工厂方法。
     * @throws InvocationTargetException
     *         如果调用工厂方式时出现错误。
     */
    public static Object buildFromFactory(
            String factoryName,
            Properties properties
    )
            throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        factoryName = notBlank(factoryName, "factoryName").trim();
        final int p = factoryName.lastIndexOf('.');
        final String factoryClassName;
        final String factoryMethodName;
        if (p >= 0) {
            factoryClassName = factoryName.substring(0, p);
            factoryMethodName = factoryName.substring(p + 1);
        } else {
            throw new IllegalArgumentException("argument \"factoryName\" does not contain method name");
        }

        final Class<?> factoryClass = Class.forName(factoryClassName);
        final Method factoryMethod = factoryClass.getMethod(factoryMethodName, Properties.class);
        factoryMethod.setAccessible(true);
        return factoryMethod.invoke(null, properties);
    }
}
